#include <string.h>
#include "gsm-watcher.h"

#define RECV_BUFFER_LENGTH 16384


// =============================================================================

static void
gsm_players_list_free (GSList *players)
{
	GSList *iter = players;
	while (iter) {
		GsmPlayer *player = iter->data;
		g_free (player->values);
		g_slice_free (GsmPlayer, player);
		iter = g_slist_next (iter);
	}
	
	g_slist_free (players);
}

static GsmPlayer *
gsm_players_list_find (GSList *players, const gchar *name)
{
	GsmPlayer *player;
	GSList *iter = players;
	
	while (iter) {
		player = iter->data;
		if (strcmp (player->name, name) == 0)
			return player;
		iter = g_slist_next (iter);
	}
	
	return NULL;
}


// =============================================================================

enum {
	PROP_0,
	PROP_ADDRESS
};

enum {
	UPDATED,
	ERROR,
	PLAYER_ONLINE,
	PLAYER_OFFLINE,
	LAST_SIGNAL
};

static GQuark
gsm_watcher_error_quark ()
{
	return g_quark_from_static_string ("gsm_watcher_error_quark");
}

static void gsm_watcher_get_property (GObject* object, guint prop_id,
		GValue* value, GParamSpec* pspec);
static void gsm_watcher_set_property (GObject* object, guint prop_id,
		const GValue* value, GParamSpec* pspec);
static void gsm_watcher_dispose (GObject *object);
static gboolean gsm_watcher_recveived (GSocket *socket, GIOCondition condition,
		gpointer user_data);

static guint signals[LAST_SIGNAL] = { 0 };
static GSocket *socket = NULL;
static GSList *watchers = NULL;

G_DEFINE_ABSTRACT_TYPE (GsmWatcher, gsm_watcher, G_TYPE_OBJECT)

static void
gsm_watcher_class_init (GsmWatcherClass *klass)
{
	GObjectClass* gobject_class = (GObjectClass *) klass;
	gobject_class->set_property = gsm_watcher_set_property;
	gobject_class->get_property = gsm_watcher_get_property;
	gobject_class->dispose = gsm_watcher_dispose;
	
	g_object_class_install_property (gobject_class, PROP_ADDRESS,
			g_param_spec_string ("address", "Address", "Server address", "",
					G_PARAM_READABLE | G_PARAM_WRITABLE));
	
	signals[UPDATED] = g_signal_new ("updated",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmWatcherClass, updated),
			NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
	
	signals[ERROR] = g_signal_new ("error",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmWatcherClass, error),
			NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
			G_TYPE_POINTER);
	
	signals[PLAYER_ONLINE] = g_signal_new ("player-online",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmWatcherClass, player_online),
			NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
			G_TYPE_POINTER);
	
	signals[PLAYER_OFFLINE] = g_signal_new ("player-offline",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmWatcherClass, player_offline),
			NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
			G_TYPE_POINTER);
	
// socket
	if (!socket) {
		socket = g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_DATAGRAM,
				G_SOCKET_PROTOCOL_UDP, NULL);
		
		GInetAddress *inet_address = g_inet_address_new_any (G_SOCKET_FAMILY_IPV4);
		GSocketAddress *sock_address = g_inet_socket_address_new (inet_address, 0);
		g_socket_bind (socket, sock_address, FALSE, NULL);
		g_object_unref (sock_address);
		g_object_unref (inet_address);
		
		GSource *src = g_socket_create_source (socket, G_IO_IN, NULL);
		g_source_set_callback (src, (GSourceFunc) gsm_watcher_recveived,
				NULL, NULL);
		g_source_attach (src, NULL);
	}
}

static void
gsm_watcher_init (GsmWatcher* watcher)
{
	watcher->timer = g_timer_new ();
	watchers = g_slist_append (watchers, watcher);
}

guint16
gsm_watcher_get_default_port (GsmWatcher *watcher)
{
	return GSM_WATCHER_GET_CLASS (watcher)->default_port;
}

static void
gsm_watcher_dispose (GObject *object)
{
	GsmWatcher *watcher = GSM_WATCHER (object);
	
	gsm_players_list_free (watcher->players);
	g_timer_destroy (watcher->timer);
	watchers = g_slist_remove (watchers, object);
	
	if (G_OBJECT_CLASS (gsm_watcher_parent_class)->dispose)
		G_OBJECT_CLASS (gsm_watcher_parent_class)->dispose (object);
}

static void
gsm_watcher_get_property (GObject* object, guint prop_id, GValue* value,
		GParamSpec* pspec)
{
	GsmWatcher *watcher = GSM_WATCHER (object);
	
	switch (prop_id) {
	case PROP_ADDRESS:
		g_value_set_string (value, gsm_watcher_get_address (watcher));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gsm_watcher_set_property (GObject* object, guint prop_id, const GValue* value,
		GParamSpec* pspec)
{
	GsmWatcher *watcher = GSM_WATCHER (object);
	
	switch (prop_id) {
	case PROP_ADDRESS:
		gsm_watcher_set_address (watcher, g_value_get_string (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

void
gsm_watcher_set_address (GsmWatcher *watcher, const gchar *address)
{
	g_return_if_fail (GSM_IS_WATCHER (watcher));
	
	if (watcher->address)
		g_free (watcher->address);
	watcher->address = g_strdup (address);
}

const gchar *
gsm_watcher_get_address (GsmWatcher *watcher)
{
	g_return_val_if_fail (GSM_IS_WATCHER (watcher), NULL);
	
	return watcher->address;
}

static void
gsm_watcher_start_timer (GsmWatcher *watcher)
{
	g_return_if_fail (GSM_IS_WATCHER (watcher));
	g_timer_reset (watcher->timer);
}

static gdouble
gsm_watcher_get_time (GsmWatcher *watcher)
{
	g_return_val_if_fail (GSM_IS_WATCHER (watcher), 0);
	return g_timer_elapsed (watcher->timer, NULL);
}

static void
gsm_watcher_stop_timer (GsmWatcher *watcher)
{
	gulong microseconds;
	g_return_if_fail (GSM_IS_WATCHER (watcher));
	g_timer_elapsed (watcher->timer, &microseconds);
	watcher->ping = microseconds / 1000;
}

static void
gsm_watcher_address_resolved_cb (GSocketAddressEnumerator *saddr_enum,
		GAsyncResult *res, GsmWatcher *watcher)
{
	GSocketAddress *sock_addr;
	GError *error = NULL;
	
	sock_addr = g_socket_address_enumerator_next_finish (saddr_enum, res, &error);
	
	if (sock_addr) {
		if (GSM_WATCHER_GET_CLASS (watcher)->resolved) {
			GInetSocketAddress *inet_sock_addr = G_INET_SOCKET_ADDRESS (sock_addr);
			GInetAddress *inet_addr = g_inet_socket_address_get_address (inet_sock_addr);
			guint16 port = g_inet_socket_address_get_port (inet_sock_addr);
			g_object_unref (sock_addr);
			
			GSM_WATCHER_GET_CLASS (watcher)->resolved (watcher, &port);
			
			sock_addr = g_inet_socket_address_new (inet_addr, port);
		}
		
		watcher->sockaddr = sock_addr;
	} else {
		g_error_free (error);
	}
}

void
gsm_watcher_update (GsmWatcher *watcher)
{
	if (!watcher->sockaddr) {
		GSocketConnectable *sconn;
		GSocketAddressEnumerator *saddr_enum;
		
		sconn = g_network_address_parse (watcher->address,
				gsm_watcher_get_default_port (watcher), NULL);
		if (sconn) {
			saddr_enum = g_socket_connectable_enumerate (sconn);
			g_socket_address_enumerator_next_async (saddr_enum, NULL,
					(GAsyncReadyCallback) gsm_watcher_address_resolved_cb,
					watcher);
		}
	} else {
		if (watcher->has_sent) {
			if (gsm_watcher_get_time (watcher) > 5) {
				GError *error = NULL;
				g_set_error_literal (&error, gsm_watcher_error_quark (),
						GSM_WATCHER_ERROR_TIMEOUT, "Timeout");
				GSM_WATCHER_GET_CLASS (watcher)->update (watcher);
				g_signal_emit (watcher, signals[ERROR], 0, error);
				g_error_free (error);
			}
		} else {
			gsm_watcher_start_timer (watcher);
			watcher->has_sent = TRUE;
			GSM_WATCHER_GET_CLASS (watcher)->update (watcher);
		}
	}
}

void
gsm_watcher_send (GsmWatcher *watcher, const gchar *buffer, gsize size)
{
	g_socket_send_to (socket, watcher->sockaddr, buffer, size, NULL, NULL);
}

static gboolean
gsm_watcher_compare_addresses (GSocketAddress *addr1, GSocketAddress *addr2)
{
	if (g_socket_address_get_family (addr1) != g_socket_address_get_family (addr2))
		return FALSE;
	
	guint16 port1 = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr1));
	guint16 port2 = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr2));
	if (port1 != port2)
		return FALSE;
	
	GInetAddress * address1 = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr1));
	GInetAddress * address2 = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr2));
	
	const guint8 *bytes1 = g_inet_address_to_bytes (address1);
	const guint8 *bytes2 = g_inet_address_to_bytes (address2);
	if (memcmp (bytes1, bytes2, g_inet_address_get_native_size (address1)))
		return FALSE;
	
	return TRUE;
}

static GsmWatcher *
gsm_watcher_find_by_sock_address (GSocketAddress *address)
{
	GSList *iter = watchers;
	
	while (iter) {
		GsmWatcher *watcher = iter->data;
		if (watcher->sockaddr && gsm_watcher_compare_addresses (address, watcher->sockaddr))
			return watcher;
		iter = g_slist_next (iter);
	}
	
	return NULL;
}

static gboolean
gsm_watcher_recveived (GSocket *socket, GIOCondition condition,
		gpointer user_data)
{
	gssize ret;
	gchar buf[RECV_BUFFER_LENGTH];
	GSocketAddress *sock_address;
	GError *error = NULL;
	
	if (condition != G_IO_IN)
		return TRUE;
	
	ret = g_socket_receive_from (socket, &sock_address, buf, RECV_BUFFER_LENGTH,
			NULL, &error);
	if (ret == -1) {
		g_message ("SOCKET ERROR: %s", error->message);
		g_error_free (error);
		return TRUE;
	}
	
	buf[ret] = 0x00;
	
	GsmWatcher *watcher = gsm_watcher_find_by_sock_address (sock_address);
	if (watcher && GSM_WATCHER_GET_CLASS (watcher)->process_respond (watcher, buf, ret)) {
		gsm_watcher_stop_timer (watcher);
		watcher->has_sent = FALSE;
		g_signal_emit (watcher, signals[UPDATED], 0);
	}
	
	g_object_unref (sock_address);
	
	return TRUE;
}

void
gsm_watcher_update_players_list (GsmWatcher *watcher, GSList *new_list)
{
	GsmPlayer *player;
	GSList *iter;
	GSList *old_list = watcher->players;
	watcher->players = new_list;
	
	for (iter = new_list; iter; iter = g_slist_next (iter)) {
		player = (GsmPlayer *) iter->data;
		if (*player->name != '\0' && !gsm_players_list_find (old_list, player->name))
			g_signal_emit (watcher, signals[PLAYER_ONLINE], 0, player);
	}
	
	for (iter = old_list; iter; iter = g_slist_next (iter)) {
		player = (GsmPlayer *) iter->data;
		if (*player->name != '\0' && !gsm_players_list_find (new_list, player->name))
			g_signal_emit (watcher, signals[PLAYER_OFFLINE], 0, player);
	}
	
	gsm_players_list_free (old_list);
}
